4.5 延迟调用
语句defer向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作。
func main() {
f,err:=os.Open("./main.go")
if err!=nil{
log.Fatalln(err)
}
defer f.Close() // 仅注册,直到main退出前才执行
...do something...
}注意,延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并缓存起来。如对状态敏感,可改用指针或闭包。
func main() {
x,y:=1,2
defer func(a int) {
println("defer x,y= ",a,y) //y为闭包引用
}(x) // 注册时复制调用参数
x+=100 // 对x的修改不会影响延迟调用
y+=200
println(x,y)
}输出:
101 202
defer x,y=1 202
延迟调用可修改当前函数命名返回值,但其自身返回值被抛弃。
多个延迟注册按FILO次序执行。
func main() {
defer println("a")
defer println("b")
}输出:
b
a
编译器通过插入额外指令来实现延迟调用执行,而return和panic语句都会终止当前函数流程,引发延迟调用。另外,return语句不是ret汇编指令,它会先更新返回值。
func test() (z int) {
defer func() {
println("defer:",z)
z+=100 // 修改命名返回值
}()
return 100 // 实际执行次序:z=100,call defer,ret
}
func main() {
println("test:",test())
}
输出:
defer:100
test:200
有关defer更详细的分析,请阅读下卷《源码剖析》。
误用
千万记住,延迟调用在函数结束时才被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。
案例:循环处理多个日志文件,不恰当的defer导致文件关闭时间延长。
func main() {
for i:=0;i<10000;i++ {
path:=fmt.Sprintf("./log/%d.txt",i)
f,err:=os.Open(path)
if err!=nil{
log.Println(err)
continue
}
// 这个关闭操作在main函数结束时才会执行,而不是当前循环中执行
// 这无端延长了逻辑结束时间和f的生命周期,平白多消耗了内存等资源
defer f.Close()
...do something...
}
}
应该直接调用,或重构为函数,将循环和处理算法分离。
func main() {
// 日志处理算法
do:=func(n int) {
path:=fmt.Sprintf("./log/%d.txt",n)
f,err:=os.Open(path)
if err!=nil{
log.Println(err)
continue
}
// 该延迟调用在此匿名函数结束时执行,而非main
defer f.Close()
...do something...
}
for i:=0;i<10000;i++ {
do(i)
}
}
性能
相比直接用CALL汇编指令调用函数,延迟调用则须花费更大代价。这其中包括注册、调用等操作,还有额外的缓存开销。
以最常用的mutex为例,我们简单对比一下两者的性能差异。
var m sync.Mutex
func call() {
m.Lock()
m.Unlock()
}
func deferCall() {
m.Lock()
defer m.Unlock()
}
func BenchmarkCall(b*testing.B) {
for i:=0;i<b.N;i++ {
call()
}
}
func BenchmarkDefer(b*testing.B) {
for i:=0;i<b.N;i++ {
deferCall()
}
}
输出:
BenchmarkCall-4 100000000 22.4 ns/op
BenchmarkDefer-4 20000000 93.8 ns/op
相差几倍的结果足以引起重视。尤其是那些性能要求高且压力大的算法,应避免使用延迟调用。